package com.fzt.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fzt.project.common.convention.exception.ClientException;
import com.fzt.project.common.convention.exception.ServiceException;
import com.fzt.project.common.enums.ValidDateTypeEnum;
import com.fzt.project.dao.entity.*;
import com.fzt.project.dao.mapper.*;
import com.fzt.project.dto.req.ShortLinkCreateReqDTO;
import com.fzt.project.dto.req.ShortLinkPageReqDTO;
import com.fzt.project.dto.req.ShortLinkUpdateReqDTO;
import com.fzt.project.dto.resp.GroupShortLinkCountRespDTO;
import com.fzt.project.dto.resp.ShortLinkCreateRespDTO;
import com.fzt.project.dto.resp.ShortLinkPageRespDTO;
import com.fzt.project.service.ShortLinkService;
import com.fzt.project.util.HashUtil;
import com.fzt.project.util.ShortLinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.fzt.project.common.constant.RedisKeyConstant.*;
import static com.fzt.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String amapKey;

    @Value("${short-link.domain.default}")
    private String defaultDomain;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqDTO) {
        //生成短链接后缀
        String shortLinkSuffix = generateSuffix(reqDTO);
        String fullShortUrl = defaultDomain+ "/" +shortLinkSuffix;
        ShortLinkDO shortLinkDO = BeanUtil.copyProperties(reqDTO, ShortLinkDO.class);
        shortLinkDO.setDomain(defaultDomain);
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setFavicon(getFavicon(reqDTO.getOriginUrl()));
        try {
            baseMapper.insert(shortLinkDO);
            ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                    .gid(shortLinkDO.getGid())
                    .fullShortUrl(shortLinkDO.getFullShortUrl())
                    .build();
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            log.warn("短链接{}重复入库", fullShortUrl);
            throw new ServiceException("短链接生成重复");
        }
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .gid(reqDTO.getGid())
                .originUrl(reqDTO.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO reqDTO) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, reqDTO.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(reqDTO, queryWrapper);
        return resultPage.convert(each ->{
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain(result.getDomain());
            return result;
        });
    }

    @Override
    public List<GroupShortLinkCountRespDTO> listGroupShortLinkCount(List<String> reqDTO) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", reqDTO)
                .eq("enable_status", 0)
                .groupBy("gid");
        /*
            SELECT
                gid as gid, count(*) as shortLinkCount
            FROM t_link
                WHERE  enable_status=?
                AND created_type=?
                AND valid_date_type=?
                AND (gid IN (?,?) AND enable_status = ?) GROUP BY gid
         */
        List<Map<String,Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList,GroupShortLinkCountRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO reqParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getFullShortUrl, reqParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO != null){
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .id(hasShortLinkDO.getId())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .favicon(hasShortLinkDO.getFavicon())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .originUrl(reqParam.getOriginUrl())
                    .describe(reqParam.getDescribe())
                    .validDate(reqParam.getValidDate())
                    .validDateType(reqParam.getValidDateType())
                    .fullShortUrl(reqParam.getFullShortUrl())
                    .build();
            if (hasShortLinkDO.getGid().equals(reqParam.getGid())){
                //未更改 gid
                shortLinkDO.setGid(reqParam.getGid());
                LambdaUpdateWrapper<ShortLinkDO> eq = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, reqParam.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, reqParam.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .set(Objects.equals(reqParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()),
                                ShortLinkDO::getValidDate, null);
                int count = baseMapper.update(shortLinkDO, eq);
                if (count < 1){
                    throw new ServiceException("更新失败");
                }
            }else {
                // gid 修改了
                baseMapper.deleteById(hasShortLinkDO.getId());
                shortLinkDO.setGid(reqParam.getGid());
                baseMapper.insert(shortLinkDO);
            }
        }else {
            throw new ClientException("短链接不存在");
        }

    }

    @SneakyThrows
    @Override
    public void restorUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each).orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;
        String orginLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        LambdaQueryWrapper<ShortLinkGotoDO> shortLinkGotoDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
        ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(shortLinkGotoDOLambdaQueryWrapper);
        if (orginLink == null){//缓存中不存在原始链接
            if (shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)){ //原链接创建过短链接
                //利用分布式锁防止缓存击穿
                RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY , fullShortUrl));
                lock.lock();
                try {
                    orginLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
                    if (StrUtil.isBlank(orginLink)){

                        if (shortLinkGotoDO == null) {
                            ((HttpServletResponse) response).sendRedirect("/page/notfound");
                            return;
                        }
                        LambdaQueryWrapper<ShortLinkDO> shortLinkDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                                .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                                .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                                .eq(ShortLinkDO::getEnableStatus, 0)
                                .eq(ShortLinkDO::getDelFlag, 0);
                        ShortLinkDO shortLinkDO = baseMapper.selectOne(shortLinkDOLambdaQueryWrapper);
                        if (shortLinkDO != null) {
                            if (shortLinkDO.getValidDate().before(new Date()) && shortLinkDO.getValidDate()!= null){
                                //已经过期并且非永久有效
                                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl),"-", 30, TimeUnit.MINUTES);
                                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                                return;
                            }
                            stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl(), ShortLinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MICROSECONDS);
                            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
                        }
                    }else {
                        ((HttpServletResponse) response).sendRedirect(orginLink);
                        //统计pv，uv
                        shortLinkStats(fullShortUrl,shortLinkGotoDO.getGid(),request,response);
                    }
                }finally {
                    lock.unlock();
                }
            }else {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
            }


        }else {
            ((HttpServletResponse) response).sendRedirect(orginLink);
            //统计pv，uv
            shortLinkStats(fullShortUrl,shortLinkGotoDO.getGid(),request,response);
        }

    }

    private void shortLinkStats(String fullShortUrl, String gid, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        try {
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable addResponseCookieTask = () -> {
                uv.set(UUID.fastUUID().toString());
                Cookie uvCookie = new Cookie("uv", uv.get());
                uvCookie.setMaxAge(60 * 60 * 24 * 30);
                uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
                ((HttpServletResponse) response).addCookie(uvCookie);
                uvFirstFlag.set(Boolean.TRUE);
                stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
            };
            if (ArrayUtil.isNotEmpty(cookies)){
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(),"uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each ->{
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                            uvFirstFlag.set(uvAdded != null && uvAdded >0L);
                        },addResponseCookieTask);
            }else {
                addResponseCookieTask.run();
            }
            //获取ip
            String actualIp = ShortLinkUtil.getActualIp((HttpServletRequest) request);
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, actualIp);
            boolean uipFirstFlag = uipAdded != null && uipAdded >0L;
            if (StrUtil.isBlank(gid)) {
                LambdaQueryWrapper<ShortLinkGotoDO> shortLinkGotoDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(shortLinkGotoDOLambdaQueryWrapper);
                gid = shortLinkGotoDO.getGid();
            }
            int hour = DateUtil.hour(new Date(),true);
            int weekday = DateUtil.dayOfWeekEnum(new Date()).getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .uip(uipFirstFlag ? 1 : 0)
                    .weekday(weekday)
                    .hour(hour)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            //调用高德api获取地区信息
            HashMap<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key",amapKey);
            localeParamMap.put("ip",actualIp);
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infocode = localeResultObj.getString("infocode");
            if ("10000".equals(infocode)){
                //正确返回
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .date(new Date())
                        .gid(gid)
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                         .province(localeResultObj.getString("province"))
                         .city(localeResultObj.getString("city"))
                         .adcode(localeResultObj.getString("adcode"))
                        .country("中国")
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
                //获取os
                String os = ShortLinkUtil.getOs((HttpServletRequest) request);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .date(new Date())
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .cnt(1)
                        .os(os)
                        .build();
                linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .date(new Date())
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .cnt(1)
                        .browser(ShortLinkUtil.getBrowser((HttpServletRequest) request))
                        .build();
                linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .ip(actualIp)
                        .os(os)
                        .user(uv.get()) //用户设置的是cookie值
                        .browser(ShortLinkUtil.getBrowser((HttpServletRequest) request))
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(ShortLinkUtil.getDevice((HttpServletRequest) request))
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .cnt(1)
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
                LinkNetworkStatsDO linkNetWorkStatsDO = LinkNetworkStatsDO.builder()
                        .network(ShortLinkUtil.getNetwork((HttpServletRequest) request))
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .cnt(1)
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetWorkStatsDO);
            }

        }catch (Exception exception){
            log.error("短链接访问量统计异常",exception);
        }
    }

    private String generateSuffix(ShortLinkCreateReqDTO reqDTO){
        int createCount = 0;
        String shortUri;
        while (true) {
            if (createCount > 10){
                throw new ClientException("短链接生成重复");
            }
            String originUrl = reqDTO.getOriginUrl();
            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(defaultDomain +"/"+shortUri) ){
                break;
            }
            createCount++;
        }
        return shortUri;
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
